home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2008 February / PCWFEB08.iso / Software / Resources / Developers / XAMPP 1.5.4 / Windows installer / xampp-win32-1.5.4-installer.exe / xampp / php / pear / HTTP / Request.php < prev    next >
Encoding:
PHP Script  |  2005-12-02  |  36.7 KB  |  1,192 lines

  1. <?php
  2. // +-----------------------------------------------------------------------+
  3. // | Copyright (c) 2002-2003, Richard Heyes                                |
  4. // | All rights reserved.                                                  |
  5. // |                                                                       |
  6. // | Redistribution and use in source and binary forms, with or without    |
  7. // | modification, are permitted provided that the following conditions    |
  8. // | are met:                                                              |
  9. // |                                                                       |
  10. // | o Redistributions of source code must retain the above copyright      |
  11. // |   notice, this list of conditions and the following disclaimer.       |
  12. // | o Redistributions in binary form must reproduce the above copyright   |
  13. // |   notice, this list of conditions and the following disclaimer in the |
  14. // |   documentation and/or other materials provided with the distribution.|
  15. // | o The names of the authors may not be used to endorse or promote      |
  16. // |   products derived from this software without specific prior written  |
  17. // |   permission.                                                         |
  18. // |                                                                       |
  19. // | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   |
  20. // | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     |
  21. // | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
  22. // | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  |
  23. // | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
  24. // | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      |
  25. // | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
  26. // | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
  27. // | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   |
  28. // | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
  29. // | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  |
  30. // |                                                                       |
  31. // +-----------------------------------------------------------------------+
  32. // | Author: Richard Heyes <richard@phpguru.org>                           |
  33. // +-----------------------------------------------------------------------+
  34. //
  35. // $Id: Request.php,v 1.43 2005/11/06 18:29:14 avb Exp $
  36. //
  37. // HTTP_Request Class
  38. //
  39. // Simple example, (Fetches yahoo.com and displays it):
  40. //
  41. // $a = &new HTTP_Request('http://www.yahoo.com/');
  42. // $a->sendRequest();
  43. // echo $a->getResponseBody();
  44. //
  45.  
  46. require_once 'PEAR.php';
  47. require_once 'Net/Socket.php';
  48. require_once 'Net/URL.php';
  49.  
  50. define('HTTP_REQUEST_METHOD_GET',     'GET',     true);
  51. define('HTTP_REQUEST_METHOD_HEAD',    'HEAD',    true);
  52. define('HTTP_REQUEST_METHOD_POST',    'POST',    true);
  53. define('HTTP_REQUEST_METHOD_PUT',     'PUT',     true);
  54. define('HTTP_REQUEST_METHOD_DELETE',  'DELETE',  true);
  55. define('HTTP_REQUEST_METHOD_OPTIONS', 'OPTIONS', true);
  56. define('HTTP_REQUEST_METHOD_TRACE',   'TRACE',   true);
  57.  
  58. define('HTTP_REQUEST_HTTP_VER_1_0', '1.0', true);
  59. define('HTTP_REQUEST_HTTP_VER_1_1', '1.1', true);
  60.  
  61. class HTTP_Request {
  62.  
  63.     /**
  64.     * Instance of Net_URL
  65.     * @var object Net_URL
  66.     */
  67.     var $_url;
  68.  
  69.     /**
  70.     * Type of request
  71.     * @var string
  72.     */
  73.     var $_method;
  74.  
  75.     /**
  76.     * HTTP Version
  77.     * @var string
  78.     */
  79.     var $_http;
  80.  
  81.     /**
  82.     * Request headers
  83.     * @var array
  84.     */
  85.     var $_requestHeaders;
  86.  
  87.     /**
  88.     * Basic Auth Username
  89.     * @var string
  90.     */
  91.     var $_user;
  92.     
  93.     /**
  94.     * Basic Auth Password
  95.     * @var string
  96.     */
  97.     var $_pass;
  98.  
  99.     /**
  100.     * Socket object
  101.     * @var object Net_Socket
  102.     */
  103.     var $_sock;
  104.     
  105.     /**
  106.     * Proxy server
  107.     * @var string
  108.     */
  109.     var $_proxy_host;
  110.     
  111.     /**
  112.     * Proxy port
  113.     * @var integer
  114.     */
  115.     var $_proxy_port;
  116.     
  117.     /**
  118.     * Proxy username
  119.     * @var string
  120.     */
  121.     var $_proxy_user;
  122.     
  123.     /**
  124.     * Proxy password
  125.     * @var string
  126.     */
  127.     var $_proxy_pass;
  128.  
  129.     /**
  130.     * Post data
  131.     * @var array
  132.     */
  133.     var $_postData;
  134.  
  135.    /**
  136.     * Request body  
  137.     * @var string
  138.     */
  139.     var $_body;
  140.  
  141.    /**
  142.     * A list of methods that MUST NOT have a request body, per RFC 2616
  143.     * @var array
  144.     */
  145.     var $_bodyDisallowed = array('TRACE');
  146.  
  147.    /**
  148.     * Files to post 
  149.     * @var array
  150.     */
  151.     var $_postFiles = array();
  152.  
  153.     /**
  154.     * Connection timeout.
  155.     * @var float
  156.     */
  157.     var $_timeout;
  158.     
  159.     /**
  160.     * HTTP_Response object
  161.     * @var object HTTP_Response
  162.     */
  163.     var $_response;
  164.     
  165.     /**
  166.     * Whether to allow redirects
  167.     * @var boolean
  168.     */
  169.     var $_allowRedirects;
  170.     
  171.     /**
  172.     * Maximum redirects allowed
  173.     * @var integer
  174.     */
  175.     var $_maxRedirects;
  176.     
  177.     /**
  178.     * Current number of redirects
  179.     * @var integer
  180.     */
  181.     var $_redirects;
  182.  
  183.    /**
  184.     * Whether to append brackets [] to array variables
  185.     * @var bool
  186.     */
  187.     var $_useBrackets = true;
  188.  
  189.    /**
  190.     * Attached listeners
  191.     * @var array
  192.     */
  193.     var $_listeners = array();
  194.  
  195.    /**
  196.     * Whether to save response body in response object property  
  197.     * @var bool
  198.     */
  199.     var $_saveBody = true;
  200.  
  201.    /**
  202.     * Timeout for reading from socket (array(seconds, microseconds))
  203.     * @var array
  204.     */
  205.     var $_readTimeout = null;
  206.  
  207.    /**
  208.     * Options to pass to Net_Socket::connect. See stream_context_create
  209.     * @var array
  210.     */
  211.     var $_socketOptions = null;
  212.  
  213.     /**
  214.     * Constructor
  215.     *
  216.     * Sets up the object
  217.     * @param    string  The url to fetch/access
  218.     * @param    array   Associative array of parameters which can have the following keys:
  219.     * <ul>
  220.     *   <li>method         - Method to use, GET, POST etc (string)</li>
  221.     *   <li>http           - HTTP Version to use, 1.0 or 1.1 (string)</li>
  222.     *   <li>user           - Basic Auth username (string)</li>
  223.     *   <li>pass           - Basic Auth password (string)</li>
  224.     *   <li>proxy_host     - Proxy server host (string)</li>
  225.     *   <li>proxy_port     - Proxy server port (integer)</li>
  226.     *   <li>proxy_user     - Proxy auth username (string)</li>
  227.     *   <li>proxy_pass     - Proxy auth password (string)</li>
  228.     *   <li>timeout        - Connection timeout in seconds (float)</li>
  229.     *   <li>allowRedirects - Whether to follow redirects or not (bool)</li>
  230.     *   <li>maxRedirects   - Max number of redirects to follow (integer)</li>
  231.     *   <li>useBrackets    - Whether to append [] to array variable names (bool)</li>
  232.     *   <li>saveBody       - Whether to save response body in response object property (bool)</li>
  233.     *   <li>readTimeout    - Timeout for reading / writing data over the socket (array (seconds, microseconds))</li>
  234.     *   <li>socketOptions  - Options to pass to Net_Socket object (array)</li>
  235.     * </ul>
  236.     * @access public
  237.     */
  238.     function HTTP_Request($url = '', $params = array())
  239.     {
  240.         $this->_sock           = &new Net_Socket();
  241.         $this->_method         =  HTTP_REQUEST_METHOD_GET;
  242.         $this->_http           =  HTTP_REQUEST_HTTP_VER_1_1;
  243.         $this->_requestHeaders = array();
  244.         $this->_postData       = array();
  245.         $this->_body           = null;
  246.  
  247.         $this->_user = null;
  248.         $this->_pass = null;
  249.  
  250.         $this->_proxy_host = null;
  251.         $this->_proxy_port = null;
  252.         $this->_proxy_user = null;
  253.         $this->_proxy_pass = null;
  254.  
  255.         $this->_allowRedirects = false;
  256.         $this->_maxRedirects   = 3;
  257.         $this->_redirects      = 0;
  258.  
  259.         $this->_timeout  = null;
  260.         $this->_response = null;
  261.  
  262.         foreach ($params as $key => $value) {
  263.             $this->{'_' . $key} = $value;
  264.         }
  265.  
  266.         if (!empty($url)) {
  267.             $this->setURL($url);
  268.         }
  269.  
  270.         // Default useragent
  271.         $this->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )');
  272.  
  273.         // Make sure keepalives dont knobble us
  274.         $this->addHeader('Connection', 'close');
  275.  
  276.         // Basic authentication
  277.         if (!empty($this->_user)) {
  278.             $this->addHeader('Authorization', 'Basic ' . base64_encode($this->_user . ':' . $this->_pass));
  279.         }
  280.  
  281.         // Use gzip encoding if possible
  282.         // Avoid gzip encoding if using multibyte functions (see #1781)
  283.         if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && extension_loaded('zlib') &&
  284.             0 == (2 & ini_get('mbstring.func_overload'))) {
  285.  
  286.             $this->addHeader('Accept-Encoding', 'gzip');
  287.         }
  288.     }
  289.     
  290.     /**
  291.     * Generates a Host header for HTTP/1.1 requests
  292.     *
  293.     * @access private
  294.     * @return string
  295.     */
  296.     function _generateHostHeader()
  297.     {
  298.         if ($this->_url->port != 80 AND strcasecmp($this->_url->protocol, 'http') == 0) {
  299.             $host = $this->_url->host . ':' . $this->_url->port;
  300.  
  301.         } elseif ($this->_url->port != 443 AND strcasecmp($this->_url->protocol, 'https') == 0) {
  302.             $host = $this->_url->host . ':' . $this->_url->port;
  303.  
  304.         } elseif ($this->_url->port == 443 AND strcasecmp($this->_url->protocol, 'https') == 0 AND strpos($this->_url->url, ':443') !== false) {
  305.             $host = $this->_url->host . ':' . $this->_url->port;
  306.         
  307.         } else {
  308.             $host = $this->_url->host;
  309.         }
  310.  
  311.         return $host;
  312.     }
  313.     
  314.     /**
  315.     * Resets the object to its initial state (DEPRECATED).
  316.     * Takes the same parameters as the constructor.
  317.     *
  318.     * @param  string $url    The url to be requested
  319.     * @param  array  $params Associative array of parameters
  320.     *                        (see constructor for details)
  321.     * @access public
  322.     * @deprecated deprecated since 1.2, call the constructor if this is necessary
  323.     */
  324.     function reset($url, $params = array())
  325.     {
  326.         $this->HTTP_Request($url, $params);
  327.     }
  328.  
  329.     /**
  330.     * Sets the URL to be requested
  331.     *
  332.     * @param  string The url to be requested
  333.     * @access public
  334.     */
  335.     function setURL($url)
  336.     {
  337.         $this->_url = &new Net_URL($url, $this->_useBrackets);
  338.  
  339.         if (!empty($this->_url->user) || !empty($this->_url->pass)) {
  340.             $this->setBasicAuth($this->_url->user, $this->_url->pass);
  341.         }
  342.  
  343.         if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http) {
  344.             $this->addHeader('Host', $this->_generateHostHeader());
  345.         }
  346.     }
  347.     
  348.     /**
  349.     * Sets a proxy to be used
  350.     *
  351.     * @param string     Proxy host
  352.     * @param int        Proxy port
  353.     * @param string     Proxy username
  354.     * @param string     Proxy password
  355.     * @access public
  356.     */
  357.     function setProxy($host, $port = 8080, $user = null, $pass = null)
  358.     {
  359.         $this->_proxy_host = $host;
  360.         $this->_proxy_port = $port;
  361.         $this->_proxy_user = $user;
  362.         $this->_proxy_pass = $pass;
  363.  
  364.         if (!empty($user)) {
  365.             $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
  366.         }
  367.     }
  368.  
  369.     /**
  370.     * Sets basic authentication parameters
  371.     *
  372.     * @param string     Username
  373.     * @param string     Password
  374.     */
  375.     function setBasicAuth($user, $pass)
  376.     {
  377.         $this->_user = $user;
  378.         $this->_pass = $pass;
  379.  
  380.         $this->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
  381.     }
  382.  
  383.     /**
  384.     * Sets the method to be used, GET, POST etc.
  385.     *
  386.     * @param string     Method to use. Use the defined constants for this
  387.     * @access public
  388.     */
  389.     function setMethod($method)
  390.     {
  391.         $this->_method = $method;
  392.     }
  393.  
  394.     /**
  395.     * Sets the HTTP version to use, 1.0 or 1.1
  396.     *
  397.     * @param string     Version to use. Use the defined constants for this
  398.     * @access public
  399.     */
  400.     function setHttpVer($http)
  401.     {
  402.         $this->_http = $http;
  403.     }
  404.  
  405.     /**
  406.     * Adds a request header
  407.     *
  408.     * @param string     Header name
  409.     * @param string     Header value
  410.     * @access public
  411.     */
  412.     function addHeader($name, $value)
  413.     {
  414.         $this->_requestHeaders[strtolower($name)] = $value;
  415.     }
  416.  
  417.     /**
  418.     * Removes a request header
  419.     *
  420.     * @param string     Header name to remove
  421.     * @access public
  422.     */
  423.     function removeHeader($name)
  424.     {
  425.         if (isset($this->_requestHeaders[strtolower($name)])) {
  426.             unset($this->_requestHeaders[strtolower($name)]);
  427.         }
  428.     }
  429.  
  430.     /**
  431.     * Adds a querystring parameter
  432.     *
  433.     * @param string     Querystring parameter name
  434.     * @param string     Querystring parameter value
  435.     * @param bool       Whether the value is already urlencoded or not, default = not
  436.     * @access public
  437.     */
  438.     function addQueryString($name, $value, $preencoded = false)
  439.     {
  440.         $this->_url->addQueryString($name, $value, $preencoded);
  441.     }    
  442.     
  443.     /**
  444.     * Sets the querystring to literally what you supply
  445.     *
  446.     * @param string     The querystring data. Should be of the format foo=bar&x=y etc
  447.     * @param bool       Whether data is already urlencoded or not, default = already encoded
  448.     * @access public
  449.     */
  450.     function addRawQueryString($querystring, $preencoded = true)
  451.     {
  452.         $this->_url->addRawQueryString($querystring, $preencoded);
  453.     }
  454.  
  455.     /**
  456.     * Adds postdata items
  457.     *
  458.     * @param string     Post data name
  459.     * @param string     Post data value
  460.     * @param bool       Whether data is already urlencoded or not, default = not
  461.     * @access public
  462.     */
  463.     function addPostData($name, $value, $preencoded = false)
  464.     {
  465.         if ($preencoded) {
  466.             $this->_postData[$name] = $value;
  467.         } else {
  468.             $this->_postData[$name] = $this->_arrayMapRecursive('urlencode', $value);
  469.         }
  470.     }
  471.  
  472.    /**
  473.     * Recursively applies the callback function to the value
  474.     * 
  475.     * @param    mixed   Callback function
  476.     * @param    mixed   Value to process
  477.     * @access   private
  478.     * @return   mixed   Processed value
  479.     */
  480.     function _arrayMapRecursive($callback, $value)
  481.     {
  482.         if (!is_array($value)) {
  483.             return call_user_func($callback, $value);
  484.         } else {
  485.             $map = array();
  486.             foreach ($value as $k => $v) {
  487.                 $map[$k] = $this->_arrayMapRecursive($callback, $v);
  488.             }
  489.             return $map;
  490.         }
  491.     }
  492.  
  493.    /**
  494.     * Adds a file to upload
  495.     * 
  496.     * This also changes content-type to 'multipart/form-data' for proper upload
  497.     * 
  498.     * @access public
  499.     * @param  string    name of file-upload field
  500.     * @param  mixed     file name(s)
  501.     * @param  mixed     content-type(s) of file(s) being uploaded
  502.     * @return bool      true on success
  503.     * @throws PEAR_Error
  504.     */
  505.     function addFile($inputName, $fileName, $contentType = 'application/octet-stream')
  506.     {
  507.         if (!is_array($fileName) && !is_readable($fileName)) {
  508.             return PEAR::raiseError("File '{$fileName}' is not readable");
  509.         } elseif (is_array($fileName)) {
  510.             foreach ($fileName as $name) {
  511.                 if (!is_readable($name)) {
  512.                     return PEAR::raiseError("File '{$name}' is not readable");
  513.                 }
  514.             }
  515.         }
  516.         $this->addHeader('Content-Type', 'multipart/form-data');
  517.         $this->_postFiles[$inputName] = array(
  518.             'name' => $fileName,
  519.             'type' => $contentType
  520.         );
  521.         return true;
  522.     }
  523.  
  524.     /**
  525.     * Adds raw postdata (DEPRECATED)
  526.     *
  527.     * @param string     The data
  528.     * @param bool       Whether data is preencoded or not, default = already encoded
  529.     * @access public
  530.     * @deprecated       deprecated since 1.3.0, method addBody() should be used instead
  531.     */
  532.     function addRawPostData($postdata, $preencoded = true)
  533.     {
  534.         $this->_body = $preencoded ? $postdata : urlencode($postdata);
  535.     }
  536.  
  537.    /**
  538.     * Sets the request body (for POST, PUT and similar requests)
  539.     *
  540.     * @param    string  Request body
  541.     * @access   public
  542.     */
  543.     function setBody($body)
  544.     {
  545.         $this->_body = $body;
  546.     }
  547.  
  548.     /**
  549.     * Clears any postdata that has been added (DEPRECATED). 
  550.     * 
  551.     * Useful for multiple request scenarios.
  552.     *
  553.     * @access public
  554.     * @deprecated deprecated since 1.2
  555.     */
  556.     function clearPostData()
  557.     {
  558.         $this->_postData = null;
  559.     }
  560.  
  561.     /**
  562.     * Appends a cookie to "Cookie:" header
  563.     * 
  564.     * @param string $name cookie name
  565.     * @param string $value cookie value
  566.     * @access public
  567.     */
  568.     function addCookie($name, $value)
  569.     {
  570.         $cookies = isset($this->_requestHeaders['cookie']) ? $this->_requestHeaders['cookie']. '; ' : '';
  571.         $this->addHeader('Cookie', $cookies . $name . '=' . $value);
  572.     }
  573.     
  574.     /**
  575.     * Clears any cookies that have been added (DEPRECATED). 
  576.     * 
  577.     * Useful for multiple request scenarios
  578.     *
  579.     * @access public
  580.     * @deprecated deprecated since 1.2
  581.     */
  582.     function clearCookies()
  583.     {
  584.         $this->removeHeader('Cookie');
  585.     }
  586.  
  587.     /**
  588.     * Sends the request
  589.     *
  590.     * @access public
  591.     * @param  bool   Whether to store response body in Response object property,
  592.     *                set this to false if downloading a LARGE file and using a Listener
  593.     * @return mixed  PEAR error on error, true otherwise
  594.     */
  595.     function sendRequest($saveBody = true)
  596.     {
  597.         if (!is_a($this->_url, 'Net_URL')) {
  598.             return PEAR::raiseError('No URL given.');
  599.         }
  600.  
  601.         $host = isset($this->_proxy_host) ? $this->_proxy_host : $this->_url->host;
  602.         $port = isset($this->_proxy_port) ? $this->_proxy_port : $this->_url->port;
  603.  
  604.         // 4.3.0 supports SSL connections using OpenSSL. The function test determines
  605.         // we running on at least 4.3.0
  606.         if (strcasecmp($this->_url->protocol, 'https') == 0 AND function_exists('file_get_contents') AND extension_loaded('openssl')) {
  607.             if (isset($this->_proxy_host)) {
  608.                 return PEAR::raiseError('HTTPS proxies are not supported.');
  609.             }
  610.             $host = 'ssl://' . $host;
  611.         }
  612.  
  613.         // magic quotes may fuck up file uploads and chunked response processing
  614.         $magicQuotes = ini_get('magic_quotes_runtime');
  615.         ini_set('magic_quotes_runtime', false);
  616.  
  617.         // If this is a second request, we may get away without
  618.         // re-connecting if they're on the same server
  619.         $err = $this->_sock->connect($host, $port, null, $this->_timeout, $this->_socketOptions);
  620.         PEAR::isError($err) or $err = $this->_sock->write($this->_buildRequest());
  621.  
  622.         if (!PEAR::isError($err)) {
  623.             if (!empty($this->_readTimeout)) {
  624.                 $this->_sock->setTimeout($this->_readTimeout[0], $this->_readTimeout[1]);
  625.             }
  626.  
  627.             $this->_notify('sentRequest');
  628.  
  629.             // Read the response
  630.             $this->_response = &new HTTP_Response($this->_sock, $this->_listeners);
  631.             $err = $this->_response->process($this->_saveBody && $saveBody);
  632.         }
  633.  
  634.         ini_set('magic_quotes_runtime', $magicQuotes);
  635.  
  636.         if (PEAR::isError($err)) {
  637.             return $err;
  638.         }
  639.  
  640.  
  641.         // Check for redirection
  642.         if (    $this->_allowRedirects
  643.             AND $this->_redirects <= $this->_maxRedirects
  644.             AND $this->getResponseCode() > 300
  645.             AND $this->getResponseCode() < 399
  646.             AND !empty($this->_response->_headers['location'])) {
  647.  
  648.             
  649.             $redirect = $this->_response->_headers['location'];
  650.  
  651.             // Absolute URL
  652.             if (preg_match('/^https?:\/\//i', $redirect)) {
  653.                 $this->_url = &new Net_URL($redirect);
  654.                 $this->addHeader('Host', $this->_generateHostHeader());
  655.             // Absolute path
  656.             } elseif ($redirect{0} == '/') {
  657.                 $this->_url->path = $redirect;
  658.             
  659.             // Relative path
  660.             } elseif (substr($redirect, 0, 3) == '../' OR substr($redirect, 0, 2) == './') {
  661.                 if (substr($this->_url->path, -1) == '/') {
  662.                     $redirect = $this->_url->path . $redirect;
  663.                 } else {
  664.                     $redirect = dirname($this->_url->path) . '/' . $redirect;
  665.                 }
  666.                 $redirect = Net_URL::resolvePath($redirect);
  667.                 $this->_url->path = $redirect;
  668.                 
  669.             // Filename, no path
  670.             } else {
  671.                 if (substr($this->_url->path, -1) == '/') {
  672.                     $redirect = $this->_url->path . $redirect;
  673.                 } else {
  674.                     $redirect = dirname($this->_url->path) . '/' . $redirect;
  675.                 }
  676.                 $this->_url->path = $redirect;
  677.             }
  678.  
  679.             $this->_redirects++;
  680.             return $this->sendRequest($saveBody);
  681.  
  682.         // Too many redirects
  683.         } elseif ($this->_allowRedirects AND $this->_redirects > $this->_maxRedirects) {
  684.             return PEAR::raiseError('Too many redirects');
  685.         }
  686.  
  687.         $this->_sock->disconnect();
  688.  
  689.         return true;
  690.     }
  691.  
  692.     /**
  693.     * Returns the response code
  694.     *
  695.     * @access public
  696.     * @return mixed     Response code, false if not set
  697.     */
  698.     function getResponseCode()
  699.     {
  700.         return isset($this->_response->_code) ? $this->_response->_code : false;
  701.     }
  702.  
  703.     /**
  704.     * Returns either the named header or all if no name given
  705.     *
  706.     * @access public
  707.     * @param string     The header name to return, do not set to get all headers
  708.     * @return mixed     either the value of $headername (false if header is not present)
  709.     *                   or an array of all headers
  710.     */
  711.     function getResponseHeader($headername = null)
  712.     {
  713.         if (!isset($headername)) {
  714.             return isset($this->_response->_headers)? $this->_response->_headers: array();
  715.         } else {
  716.             $headername = strtolower($headername);
  717.             return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false;
  718.         }
  719.     }
  720.  
  721.     /**
  722.     * Returns the body of the response
  723.     *
  724.     * @access public
  725.     * @return mixed     response body, false if not set
  726.     */
  727.     function getResponseBody()
  728.     {
  729.         return isset($this->_response->_body) ? $this->_response->_body : false;
  730.     }
  731.  
  732.     /**
  733.     * Returns cookies set in response
  734.     * 
  735.     * @access public
  736.     * @return mixed     array of response cookies, false if none are present
  737.     */
  738.     function getResponseCookies()
  739.     {
  740.         return isset($this->_response->_cookies) ? $this->_response->_cookies : false;
  741.     }
  742.  
  743.     /**
  744.     * Builds the request string
  745.     *
  746.     * @access private
  747.     * @return string The request string
  748.     */
  749.     function _buildRequest()
  750.     {
  751.         $separator = ini_get('arg_separator.output');
  752.         ini_set('arg_separator.output', '&');
  753.         $querystring = ($querystring = $this->_url->getQueryString()) ? '?' . $querystring : '';
  754.         ini_set('arg_separator.output', $separator);
  755.  
  756.         $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : '';
  757.         $port = (isset($this->_proxy_host) AND $this->_url->port != 80) ? ':' . $this->_url->port : '';
  758.         $path = (empty($this->_url->path)? '/': $this->_url->path) . $querystring;
  759.         $url  = $host . $port . $path;
  760.  
  761.         $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n";
  762.  
  763.         if (in_array($this->_method, $this->_bodyDisallowed) ||
  764.             (HTTP_REQUEST_METHOD_POST != $this->_method && empty($this->_body)) ||
  765.             (HTTP_REQUEST_METHOD_POST != $this->_method && empty($this->_postData) && empty($this->_postFiles))) {
  766.  
  767.             $this->removeHeader('Content-Type');
  768.         } else {
  769.             if (empty($this->_requestHeaders['content-type'])) {
  770.                 // Add default content-type
  771.                 $this->addHeader('Content-Type', 'application/x-www-form-urlencoded');
  772.             } elseif ('multipart/form-data' == $this->_requestHeaders['content-type']) {
  773.                 $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime());
  774.                 $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary);
  775.             }
  776.         }
  777.  
  778.         // Request Headers
  779.         if (!empty($this->_requestHeaders)) {
  780.             foreach ($this->_requestHeaders as $name => $value) {
  781.                 $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
  782.                 $request      .= $canonicalName . ': ' . $value . "\r\n";
  783.             }
  784.         }
  785.  
  786.         // No post data or wrong method, so simply add a final CRLF
  787.         if (in_array($this->_method, $this->_bodyDisallowed) || 
  788.             (HTTP_REQUEST_METHOD_POST != $this->_method && empty($this->_body))) {
  789.  
  790.             $request .= "\r\n";
  791.  
  792.         // Post data if it's an array
  793.         } elseif (HTTP_REQUEST_METHOD_POST == $this->_method && 
  794.                   (!empty($this->_postData) || !empty($this->_postFiles))) {
  795.  
  796.             // "normal" POST request
  797.             if (!isset($boundary)) {
  798.                 $postdata = implode('&', array_map(
  799.                     create_function('$a', 'return $a[0] . \'=\' . $a[1];'), 
  800.                     $this->_flattenArray('', $this->_postData)
  801.                 ));
  802.  
  803.             // multipart request, probably with file uploads
  804.             } else {
  805.                 $postdata = '';
  806.                 if (!empty($this->_postData)) {
  807.                     $flatData = $this->_flattenArray('', $this->_postData);
  808.                     foreach ($flatData as $item) {
  809.                         $postdata .= '--' . $boundary . "\r\n";
  810.                         $postdata .= 'Content-Disposition: form-data; name="' . $item[0] . '"';
  811.                         $postdata .= "\r\n\r\n" . urldecode($item[1]) . "\r\n";
  812.                     }
  813.                 }
  814.                 foreach ($this->_postFiles as $name => $value) {
  815.                     if (is_array($value['name'])) {
  816.                         $varname       = $name . ($this->_useBrackets? '[]': '');
  817.                     } else {
  818.                         $varname       = $name;
  819.                         $value['name'] = array($value['name']);
  820.                     }
  821.                     foreach ($value['name'] as $key => $filename) {
  822.                         $fp   = fopen($filename, 'r');
  823.                         $data = fread($fp, filesize($filename));
  824.                         fclose($fp);
  825.                         $basename = basename($filename);
  826.                         $type     = is_array($value['type'])? @$value['type'][$key]: $value['type'];
  827.  
  828.                         $postdata .= '--' . $boundary . "\r\n";
  829.                         $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"';
  830.                         $postdata .= "\r\nContent-Type: " . $type;
  831.                         $postdata .= "\r\n\r\n" . $data . "\r\n";
  832.                     }
  833.                 }
  834.                 $postdata .= '--' . $boundary . "--\r\n";
  835.             }
  836.             $request .= 'Content-Length: ' . strlen($postdata) . "\r\n\r\n";
  837.             $request .= $postdata;
  838.  
  839.         // Explicitly set request body
  840.         } elseif (!empty($this->_body)) {
  841.  
  842.             $request .= 'Content-Length: ' . strlen($this->_body) . "\r\n\r\n";
  843.             $request .= $this->_body;
  844.         }
  845.         
  846.         return $request;
  847.     }
  848.  
  849.    /**
  850.     * Helper function to change the (probably multidimensional) associative array
  851.     * into the simple one.
  852.     *
  853.     * @param    string  name for item
  854.     * @param    mixed   item's values
  855.     * @return   array   array with the following items: array('item name', 'item value');
  856.     */
  857.     function _flattenArray($name, $values)
  858.     {
  859.         if (!is_array($values)) {
  860.             return array(array($name, $values));
  861.         } else {
  862.             $ret = array();
  863.             foreach ($values as $k => $v) {
  864.                 if (empty($name)) {
  865.                     $newName = $k;
  866.                 } elseif ($this->_useBrackets) {
  867.                     $newName = $name . '[' . $k . ']';
  868.                 } else {
  869.                     $newName = $name;
  870.                 }
  871.                 $ret = array_merge($ret, $this->_flattenArray($newName, $v));
  872.             }
  873.             return $ret;
  874.         }
  875.     }
  876.  
  877.  
  878.    /**
  879.     * Adds a Listener to the list of listeners that are notified of
  880.     * the object's events
  881.     * 
  882.     * @param    object   HTTP_Request_Listener instance to attach
  883.     * @return   boolean  whether the listener was successfully attached
  884.     * @access   public
  885.     */
  886.     function attach(&$listener)
  887.     {
  888.         if (!is_a($listener, 'HTTP_Request_Listener')) {
  889.             return false;
  890.         }
  891.         $this->_listeners[$listener->getId()] =& $listener;
  892.         return true;
  893.     }
  894.  
  895.  
  896.    /**
  897.     * Removes a Listener from the list of listeners 
  898.     * 
  899.     * @param    object   HTTP_Request_Listener instance to detach
  900.     * @return   boolean  whether the listener was successfully detached
  901.     * @access   public
  902.     */
  903.     function detach(&$listener)
  904.     {
  905.         if (!is_a($listener, 'HTTP_Request_Listener') || 
  906.             !isset($this->_listeners[$listener->getId()])) {
  907.             return false;
  908.         }
  909.         unset($this->_listeners[$listener->getId()]);
  910.         return true;
  911.     }
  912.  
  913.  
  914.    /**
  915.     * Notifies all registered listeners of an event.
  916.     * 
  917.     * Events sent by HTTP_Request object
  918.     * 'sentRequest': after the request was sent
  919.     * Events sent by HTTP_Response object
  920.     * 'gotHeaders': after receiving response headers (headers are passed in $data)
  921.     * 'tick': on receiving a part of response body (the part is passed in $data)
  922.     * 'gzTick': on receiving a gzip-encoded part of response body (ditto)
  923.     * 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped)
  924.     * 
  925.     * @param    string  Event name
  926.     * @param    mixed   Additional data
  927.     * @access   private
  928.     */
  929.     function _notify($event, $data = null)
  930.     {
  931.         foreach (array_keys($this->_listeners) as $id) {
  932.             $this->_listeners[$id]->update($this, $event, $data);
  933.         }
  934.     }
  935. }
  936.  
  937.  
  938. /**
  939. * Response class to complement the Request class
  940. */
  941. class HTTP_Response
  942. {
  943.     /**
  944.     * Socket object
  945.     * @var object
  946.     */
  947.     var $_sock;
  948.  
  949.     /**
  950.     * Protocol
  951.     * @var string
  952.     */
  953.     var $_protocol;
  954.     
  955.     /**
  956.     * Return code
  957.     * @var string
  958.     */
  959.     var $_code;
  960.     
  961.     /**
  962.     * Response headers
  963.     * @var array
  964.     */
  965.     var $_headers;
  966.  
  967.     /**
  968.     * Cookies set in response  
  969.     * @var array
  970.     */
  971.     var $_cookies;
  972.  
  973.     /**
  974.     * Response body
  975.     * @var string
  976.     */
  977.     var $_body = '';
  978.  
  979.    /**
  980.     * Used by _readChunked(): remaining length of the current chunk
  981.     * @var string
  982.     */
  983.     var $_chunkLength = 0;
  984.  
  985.    /**
  986.     * Attached listeners
  987.     * @var array
  988.     */
  989.     var $_listeners = array();
  990.  
  991.     /**
  992.     * Constructor
  993.     *
  994.     * @param  object Net_Socket     socket to read the response from
  995.     * @param  array                 listeners attached to request
  996.     * @return mixed PEAR Error on error, true otherwise
  997.     */
  998.     function HTTP_Response(&$sock, &$listeners)
  999.     {
  1000.         $this->_sock      =& $sock;
  1001.         $this->_listeners =& $listeners;
  1002.     }
  1003.  
  1004.  
  1005.    /**
  1006.     * Processes a HTTP response
  1007.     * 
  1008.     * This extracts response code, headers, cookies and decodes body if it 
  1009.     * was encoded in some way
  1010.     *
  1011.     * @access public
  1012.     * @param  bool      Whether to store response body in object property, set
  1013.     *                   this to false if downloading a LARGE file and using a Listener.
  1014.     *                   This is assumed to be true if body is gzip-encoded.
  1015.     * @throws PEAR_Error
  1016.     * @return mixed     true on success, PEAR_Error in case of malformed response
  1017.     */
  1018.     function process($saveBody = true)
  1019.     {
  1020.         do {
  1021.             $line = $this->_sock->readLine();
  1022.             if (sscanf($line, 'HTTP/%s %s', $http_version, $returncode) != 2) {
  1023.                 return PEAR::raiseError('Malformed response.');
  1024.             } else {
  1025.                 $this->_protocol = 'HTTP/' . $http_version;
  1026.                 $this->_code     = intval($returncode);
  1027.             }
  1028.             while ('' !== ($header = $this->_sock->readLine())) {
  1029.                 $this->_processHeader($header);
  1030.             }
  1031.         } while (100 == $this->_code);
  1032.  
  1033.         $this->_notify('gotHeaders', $this->_headers);
  1034.  
  1035.         // If response body is present, read it and decode
  1036.         $chunked = isset($this->_headers['transfer-encoding']) && ('chunked' == $this->_headers['transfer-encoding']);
  1037.         $gzipped = isset($this->_headers['content-encoding']) && ('gzip' == $this->_headers['content-encoding']);
  1038.         $hasBody = false;
  1039.         if (!isset($this->_headers['content-length']) || 0 != $this->_headers['content-length']) {
  1040.             while (!$this->_sock->eof()) {
  1041.                 if ($chunked) {
  1042.                     $data = $this->_readChunked();
  1043.                 } else {
  1044.                     $data = $this->_sock->read(4096);
  1045.                 }
  1046.                 if ('' == $data) {
  1047.                     break;
  1048.                 } else {
  1049.                     $hasBody = true;
  1050.                     if ($saveBody || $gzipped) {
  1051.                         $this->_body .= $data;
  1052.                     }
  1053.                     $this->_notify($gzipped? 'gzTick': 'tick', $data);
  1054.                 }
  1055.             }
  1056.         }
  1057.         if ($hasBody) {
  1058.             // Uncompress the body if needed
  1059.             if ($gzipped) {
  1060.                 $this->_body = gzinflate(substr($this->_body, 10));
  1061.                 $this->_notify('gotBody', $this->_body);
  1062.             } else {
  1063.                 $this->_notify('gotBody');
  1064.             }
  1065.         }
  1066.         return true;
  1067.     }
  1068.  
  1069.  
  1070.    /**
  1071.     * Processes the response header
  1072.     *
  1073.     * @access private
  1074.     * @param  string    HTTP header
  1075.     */
  1076.     function _processHeader($header)
  1077.     {
  1078.         list($headername, $headervalue) = explode(':', $header, 2);
  1079.         $headername  = strtolower($headername);
  1080.         $headervalue = ltrim($headervalue);
  1081.         
  1082.         if ('set-cookie' != $headername) {
  1083.             if (isset($this->_headers[$headername])) {
  1084.                 $this->_headers[$headername] .= ',' . $headervalue;
  1085.             } else {
  1086.                 $this->_headers[$headername]  = $headervalue;
  1087.             }
  1088.         } else {
  1089.             $this->_parseCookie($headervalue);
  1090.         }
  1091.     }
  1092.  
  1093.  
  1094.    /**
  1095.     * Parse a Set-Cookie header to fill $_cookies array
  1096.     *
  1097.     * @access private
  1098.     * @param  string    value of Set-Cookie header
  1099.     */
  1100.     function _parseCookie($headervalue)
  1101.     {
  1102.         $cookie = array(
  1103.             'expires' => null,
  1104.             'domain'  => null,
  1105.             'path'    => null,
  1106.             'secure'  => false
  1107.         );
  1108.  
  1109.         // Only a name=value pair
  1110.         if (!strpos($headervalue, ';')) {
  1111.             $pos = strpos($headervalue, '=');
  1112.             $cookie['name']  = trim(substr($headervalue, 0, $pos));
  1113.             $cookie['value'] = trim(substr($headervalue, $pos + 1));
  1114.  
  1115.         // Some optional parameters are supplied
  1116.         } else {
  1117.             $elements = explode(';', $headervalue);
  1118.             $pos = strpos($elements[0], '=');
  1119.             $cookie['name']  = trim(substr($elements[0], 0, $pos));
  1120.             $cookie['value'] = trim(substr($elements[0], $pos + 1));
  1121.  
  1122.             for ($i = 1; $i < count($elements); $i++) {
  1123.                 if (false === strpos($elements[$i], '=')) {
  1124.                     $elName  = trim($elements[$i]);
  1125.                     $elValue = null;
  1126.                 } else {
  1127.                     list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
  1128.                 }
  1129.                 $elName = strtolower($elName);
  1130.                 if ('secure' == $elName) {
  1131.                     $cookie['secure'] = true;
  1132.                 } elseif ('expires' == $elName) {
  1133.                     $cookie['expires'] = str_replace('"', '', $elValue);
  1134.                 } elseif ('path' == $elName || 'domain' == $elName) {
  1135.                     $cookie[$elName] = urldecode($elValue);
  1136.                 } else {
  1137.                     $cookie[$elName] = $elValue;
  1138.                 }
  1139.             }
  1140.         }
  1141.         $this->_cookies[] = $cookie;
  1142.     }
  1143.  
  1144.  
  1145.    /**
  1146.     * Read a part of response body encoded with chunked Transfer-Encoding
  1147.     * 
  1148.     * @access private
  1149.     * @return string
  1150.     */
  1151.     function _readChunked()
  1152.     {
  1153.         // at start of the next chunk?
  1154.         if (0 == $this->_chunkLength) {
  1155.             $line = $this->_sock->readLine();
  1156.             if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) {
  1157.                 $this->_chunkLength = hexdec($matches[1]); 
  1158.                 // Chunk with zero length indicates the end
  1159.                 if (0 == $this->_chunkLength) {
  1160.                     $this->_sock->readLine(); // make this an eof()
  1161.                     return '';
  1162.                 }
  1163.             } else {
  1164.                 return '';
  1165.             }
  1166.         }
  1167.         $data = $this->_sock->read($this->_chunkLength);
  1168.         $this->_chunkLength -= strlen($data);
  1169.         if (0 == $this->_chunkLength) {
  1170.             $this->_sock->readLine(); // Trailing CRLF
  1171.         }
  1172.         return $data;
  1173.     }
  1174.  
  1175.  
  1176.    /**
  1177.     * Notifies all registered listeners of an event.
  1178.     * 
  1179.     * @param    string  Event name
  1180.     * @param    mixed   Additional data
  1181.     * @access   private
  1182.     * @see HTTP_Request::_notify()
  1183.     */
  1184.     function _notify($event, $data = null)
  1185.     {
  1186.         foreach (array_keys($this->_listeners) as $id) {
  1187.             $this->_listeners[$id]->update($this, $event, $data);
  1188.         }
  1189.     }
  1190. } // End class HTTP_Response
  1191. ?>
  1192.